<?php

/*

HTTPSEverywhere for Desktopd
Copyright (C) 2015  Desktopd Developer(s)

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

https://www.gnu.org/licenses/agpl.html

*/


namespace Desktopd\HTTPSEverywhere;


// NOTE: Memory hungry
class HTTPSEverywhere implements \Countable {
    protected $platforms = array('firefox', 'mixedcontent');
    
    protected $rulesets = array(); // indexed by names
    protected $hostIndex = array(); // indexed by host patterns
    
    protected $lastRulesetName = '(none)';
    protected $forceHTTPS = false;
    
    
    public function __construct ($ruleDir, $userRuleDir = false) {
        $ruleFiles = array();
        if (is_dir($ruleDir)) {
            $ruleFiles = glob("$ruleDir/*.xml");
            
            if (!is_array($ruleFiles)) {
                $ruleFiles = array();
            }
        }
        
        if (false !== $userRuleDir && is_dir($userRuleDir)) {
            $userRuleFiles = glob("$userRuleDir/*.xml");
            
            if (is_array($userRuleFiles)) {
                $ruleFiles = array_merge($ruleFiles, $userRuleFiles);
            }
        }
        
        foreach ($ruleFiles as $file) {
            if (!is_file($file)) continue;
            if (!is_readable($file)) continue;
            
            $data = file_get_contents($file);
            if (!$data) continue;
            
            try {
                $doc = new \DOMDocument();
                $doc->loadXML($data);
                unset($data);
                
                $ruleSet = $doc->documentElement;
                if ($ruleSet->tagName !== 'ruleset') {
                    throw new \RuntimeException('Invalid rule');
                }
                
                $name = $ruleSet->getAttribute('name');
                $obj = new HTTPSEverywhereRuleset($name);
                $this->rulesets[$name] = $obj;
                
                $disabled = $ruleSet->getAttribute('default_off');
                if ($disabled) {
                    $obj->disable();
                }
                
                $platforms = preg_split('/\s+/', $ruleSet->getAttribute('platform'). null, PREG_SPLIT_NO_EMPTY);
                foreach ($platforms as $platform) {
                    $obj->addPlatform($platform);
                }
                
                $targets = $ruleSet->getElementsByTagName('target');
                foreach ($targets as $target) {
                    $host = $target->getAttribute('host');
                    if ($host == '') continue;
                    
                    $this->hostIndex[$host] = $obj;
                }
                
                $exclusions = $ruleSet->getElementsByTagName('exclusion');
                foreach ($exclusions as $exclusion) {
                    $pattern = $exclusion->getAttribute('pattern');
                    if ($pattern == '') continue;
                    
                    $obj->addExclusion($pattern);
                }
                
                $rules = $ruleSet->getElementsByTagName('rule');
                foreach ($rules as $rule) {
                    $from = $rule->getAttribute('from');
                    if ($from == '') continue;
                    
                    $to = $rule->getAttribute('to');
                    if ($to == '') continue;
                    
                    $downgradeAllowed = $rule->getAttribute('downgrade');
                    $obj->addPattern($from, $to, (bool) $downgradeAllowed);
                }
                
            } catch (\DOMException $e) {
                continue;
            } catch (\RuntimeException $e) {
                continue;
            }
        }
        unset($file, $data, $doc);
        
    }
    
    public function addPlatform ($platform) {
        if (!in_array($platform, $this->platforms, true)) $this->platforms[] = $platform;
        return count($this->platforms);
    }
    
    public function removePlatform ($platform) {
        foreach (array_keys($this->platforms, $platform, true) as $key) {
            unset($this->platforms[$key]);
        }
        $this->platforms = array_values($this->platforms);
        return count($this->platforms);
    }
    
    public function hasPlatform ($platform) {
        return in_array($platform, $this->platforms, true);
    }
    
    protected function extractHost ($uri) {
        if (preg_match('~^(?:\w+?:)?//(?:[^@:/]+(?:\:[^@:/]+)?@)?([^/]+)(?:/.*)?$~', $uri, $matches)) {
            return $matches[1];
        }
        
        return '';
    }
    
    public function forceHTTPS ($on = true) {
        return $this->forceHTTPS = (bool) $on;
    }
    
    public function isHTTPSForced () {
        return $this->forceHTTPS;
    }
    
    public function rewrite ($uri, $maxVisit = 3) {
        if ($this->forceHTTPS) {
            return preg_replace('~^(?:https?:)?//~', 'https://', $uri);
        }
        
        $visitedRulesets = new \SplObjectStorage();
        
        while (true) {
            $host = $this->extractHost($uri);
            
            foreach ($this->hostIndex as $pattern => $ruleset) {
                if (!fnmatch($pattern, $host)) continue;
                if (!($ruleset instanceof HTTPSEverywhereRuleset)) continue;
                
                if (isset($visitedRulesets[$ruleset])) {
                    if ($visitedRulesets[$ruleset] >= $maxVisit) {
                        continue;
                    }
                }
                
                $supported = false;
                foreach ($this->platforms as $platform) {
                    if ($ruleset->platformSupported($platform)) {
                        $supported = true;
                        break;
                    }
                }
                
                if (!$supported) {
                    continue;
                }
                
                // rewrite
                $rewritten = $ruleset->rewrite($uri);
                
                // visit count to avoid loops
                // you cannot create infinite loops without repeating
                // the same ruleset(s)
                if (!isset($visitedRulesets[$ruleset])) {
                    $visitedRulesets[$ruleset] = 1;
                } else {
                    $visitedRulesets[$ruleset] = 1 + $visitedRulesets[$ruleset];
                }
                
                if ($rewritten === $uri) {
                    continue; // skip this rule
                }
                
                $uri = $rewritten;
                $this->lastRulesetName = $ruleset->getName();
                
                continue 2; // restart 'while'
            }
            
            // no matches
            break;
        }
        
        return $uri;
    }
    
    public function getLastRulesetName () {
        return $this->lastRulesetName;
    }
    
    public function count () {
        return count($this->rulesets);
    }
}


// vim: ts=4 et ai

